implemented "Subscribe To Topic"

This commit is contained in:
Eric J. Bowersox 2001-11-29 04:55:48 +00:00
parent c7edf62aae
commit 239321bb61
18 changed files with 430 additions and 14 deletions

View File

@ -438,6 +438,34 @@ Hope to see you in "${community.name}" soon!
<!-- Parameters: community.name = name of community -->
<subj-invite>Invitation to "${community.name}" Community</subj-invite>
<!-- Template used for E-mailing posts to users (top half of E-mail) -->
<!-- Parameters: message.poster, topic.name, topic.locator, conference.name, community.name -->
<mail-post-top>
<![CDATA[
The following message was just posted by "${message.poster}" to the
"${topic.name}" topic in the "${conference.name}" conference
of the "${community.name}" community on the Venice community system.
--------------------------------------------------------------------------
]]>
</mail-post-top>
<!-- Template used for E-mailing posts to users (bottom half of E-mail) -->
<!-- Parameters: message.poster, topic.name, topic.locator, conference.name, community.name -->
<mail-post-bottom>
<![CDATA[
--------------------------------------------------------------------------
Join the ongoing discussion at:
http://delenn/venice/go/${topic.locator}
To stop receiving new posts in this topic by E-mail, visit the above URL,
click the "Manage" button, and click the "Stop Subscribing To This Topic" link.
]]>
</mail-post-bottom>
<!-- Subject line for posts that are E-mailed to users -->
<!-- Parameters: message.poster, topic.name, topic.locator, conference.name, community.name -->
<subj-mail-post>New Post in ${topic.name}</subj-mail-post>
</messages>
</venice-config>

View File

@ -335,6 +335,7 @@ CREATE TABLE topicsettings (
last_message INT DEFAULT -1,
last_read DATETIME,
last_post DATETIME,
subscribe TINYINT DEFAULT 0,
PRIMARY KEY (topicid, uid)
);

View File

@ -100,5 +100,9 @@ public interface TopicContext
public abstract List getBozos() throws DataException;
public abstract boolean isSubscribed();
public abstract void setSubscribed(boolean flag) throws DataException;
} // end interface TopicContext

View File

@ -1458,6 +1458,12 @@ class CommunityUserContextImpl implements CommunityContext, CommunityBackend
} // end env_testPermission
public String env_getCommunityName()
{
return this.getName();
} // end env.getCommunityName()
/*--------------------------------------------------------------------------------
* Static operations for use within the implementation package
*--------------------------------------------------------------------------------

View File

@ -1612,6 +1612,12 @@ class ConferenceUserContextImpl implements ConferenceContext, ConferenceBackend
} // end env_getConfLevel
public String env_getConfName()
{
return this.getName();
} // end env_getConfName
/*--------------------------------------------------------------------------------
* Static functions usable only from within the package
*--------------------------------------------------------------------------------

View File

@ -0,0 +1,157 @@
/*
* The contents of this file are subject to the Mozilla Public License Version 1.1
* (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at <http://www.mozilla.org/MPL/>.
*
* Software distributed under the License is distributed on an "AS IS" basis, WITHOUT
* WARRANTY OF ANY KIND, either express or implied. See the License for the specific
* language governing rights and limitations under the License.
*
* The Original Code is the Venice Web Communities System.
*
* The Initial Developer of the Original Code is Eric J. Bowersox <erbo@silcom.com>,
* for Silverwrist Design Studios. Portions created by Eric J. Bowersox are
* Copyright (C) 2001 Eric J. Bowersox/Silverwrist Design Studios. All Rights Reserved.
*
* Contributor(s):
*/
package com.silverwrist.venice.core.impl;
import java.util.*;
import org.apache.log4j.*;
import com.silverwrist.util.StringUtil;
import com.silverwrist.venice.core.internals.*;
import com.silverwrist.venice.except.*;
import com.silverwrist.venice.htmlcheck.*;
class PostDeliveryAgent extends Thread
{
/*--------------------------------------------------------------------------------
* Static data members
*--------------------------------------------------------------------------------
*/
private static Category logger = Category.getInstance(PostDeliveryAgent.class);
/*--------------------------------------------------------------------------------
* Attributes
*--------------------------------------------------------------------------------
*/
private EnvEngine env; // the environment
private String post_text; // the raw post text
private String post_pseud; // the raw pseud
private List delivery_addresses; // the addresses to deliver to
private Map vars; // the replacement variables for the templates
/*--------------------------------------------------------------------------------
* Constructor
*--------------------------------------------------------------------------------
*/
PostDeliveryAgent(EnvConference env, String txt, String pseud, String topicname, int topicnum, List addrs)
{
super("PostDeliveryAgent");
setDaemon(false);
// Save the calling data.
this.env = env;
this.post_text = txt;
this.post_pseud = pseud;
this.delivery_addresses = addrs;
// Now build the substitution table used to format the templates.
HashMap xvars = new HashMap();
xvars.put("message.poster",env.getUserName());
xvars.put("topic.name",topicname);
xvars.put("topic.locator",env.getCommunityAlias() + "!" + env.getConferenceAlias() + "." + topicnum);
xvars.put("conference.name",env.getConferenceName());
xvars.put("community.name",env.getCommunityName());
vars = Collections.unmodifiableMap(xvars);
} // end constructor
/*--------------------------------------------------------------------------------
* Overrides from class Thread
*--------------------------------------------------------------------------------
*/
public void run()
{
NDC.push("PostDeliveryAgent");
try
{ // kick off the delivery process
if (logger.isDebugEnabled())
logger.debug("PostDeliveryAgent started by " + vars.get("message.poster") + " with "
+ delivery_addresses.size() + " mails to deliver");
// To start off with, both the text and the pseud need to be "stripped" of all HTML.
String real_text = null;
String real_pseud = null;
try
{ // run both arguments through the HTML checker
HTMLChecker mail_checker = env.getEngine().createCheckerObject(EngineBackend.HTMLC_MAIL_POST);
mail_checker.append(post_text);
mail_checker.finish();
real_text = mail_checker.getValue();
mail_checker.reset();
mail_checker.append(post_pseud);
mail_checker.finish();
real_pseud = mail_checker.getValue();
} // end try
catch (HTMLCheckerException e)
{ // this isn't right...
logger.error("PostDeliveryAgent: threw HTMLCheckerException",e);
throw new InternalStateError("HTMLChecker erroneously throwing exception",e);
} // end catch
// Format the three stock message parts.
String msg_top = StringUtil.replaceAllVariables(env.getStockMessage("mail-post-top"),vars);
String msg_bot = StringUtil.replaceAllVariables(env.getStockMessage("mail-post-bottom"),vars);
String msg_subj = StringUtil.replaceAllVariables(env.getStockMessage("subj-mail-post"),vars);
// Construct the full message body.
StringBuffer body = new StringBuffer(msg_top);
body.append("\n\n").append(real_pseud).append("\n\n").append(real_text).append("\n\n");
body.append(msg_bot).append("\n--\n").append(env.getStockMessage("signature"));
// Create the emailer object and load it up with most of the data.
Emailer em = env.getEngine().createEmailer();
em.setSubject(msg_subj);
em.setText(body.toString());
// Deliver the mail to each of the target addresses in turn.
Iterator it = delivery_addresses.iterator();
while (it.hasNext())
{ // get ready to deliver the mail...
String addr = (String)(it.next());
if (logger.isDebugEnabled())
logger.debug("Delivering to " + addr);
try
{ // set the recipient and send it out
em.setTo(addr);
em.send();
} // end try
catch (EmailException e)
{ // log the error and move on
logger.error("Caught EmailException when trying to send to " + addr,e);
} // end catch
} // end while
} // end try
finally
{ // pop the nested diagnostic context before we go
NDC.pop();
} // end finally
} // end run
} // end class PostDeliveryAgent

View File

@ -20,6 +20,7 @@ package com.silverwrist.venice.core.impl;
import java.sql.*;
import java.util.*;
import org.apache.log4j.*;
import com.silverwrist.util.StringUtil;
import com.silverwrist.venice.core.*;
import com.silverwrist.venice.core.internals.*;
import com.silverwrist.venice.db.*;
@ -53,6 +54,7 @@ class TopicUserContextImpl implements TopicContext
private String name;
private boolean hidden;
private int unread;
private boolean subscribed;
private boolean deleted = false;
private HashSet bozo_uids = null;
@ -63,7 +65,8 @@ class TopicUserContextImpl implements TopicContext
protected TopicUserContextImpl(EnvConference env, int topicid, short topicnum, int creator_uid,
int top_message, boolean frozen, boolean archived, java.util.Date created,
java.util.Date lastupdate, String name, boolean hidden, int unread)
java.util.Date lastupdate, String name, boolean hidden, int unread,
boolean subscribed)
{
this.env = env;
this.topicid = topicid;
@ -77,6 +80,7 @@ class TopicUserContextImpl implements TopicContext
this.name = name;
this.hidden = hidden;
this.unread = (unread<0 ? 0 : unread);
this.subscribed = subscribed;
} // end constructor
@ -94,6 +98,7 @@ class TopicUserContextImpl implements TopicContext
this.name = name;
this.hidden = false;
this.unread = 1;
this.subscribed = false;
this.bozo_uids = new HashSet(); // no bozos yet
} // end constructor
@ -109,7 +114,8 @@ class TopicUserContextImpl implements TopicContext
new StringBuffer("SELECT topics.topicid, topics.num, topics.creator_uid, topics.top_message, "
+ "topics.frozen, topics.archived, topics.createdate, topics.lastupdate, "
+ "topics.name, IFNULL(topicsettings.hidden,0) AS hidden, "
+ "(topics.top_message - IFNULL(topicsettings.last_message,-1)) AS unread "
+ "(topics.top_message - IFNULL(topicsettings.last_message,-1)) AS unread, "
+ "IFNULL(topicsettings.subscribe,0) AS subscribe "
+ "FROM topics LEFT JOIN topicsettings ON topics.topicid = topicsettings.topicid "
+ "AND topicsettings.uid = ");
sql.append(uid).append(" WHERE topics.topicid = ").append(topicid).append(';');
@ -129,6 +135,7 @@ class TopicUserContextImpl implements TopicContext
name = null;
hidden = false;
unread = -1;
subscribed = false;
deleted = true;
} // end if
@ -149,6 +156,7 @@ class TopicUserContextImpl implements TopicContext
unread = rs.getInt(11);
if (unread<0)
unread = 0;
subscribed = rs.getBoolean(12);
} // end if
else // this topic must have been deleted - fsck it
@ -666,11 +674,15 @@ class TopicUserContextImpl implements TopicContext
java.util.Date posted_date;
Connection conn = null;
AuditRecord ar = null;
ArrayList mailto_addrs = null;
try
{ // get a database connection
conn = env.getConnection();
Statement stmt = conn.createStatement();
ArrayList mailto_uids = null;
StringBuffer sql = new StringBuffer();
ResultSet rs;
// slap a lock on all the tables we need to touch
stmt.executeUpdate("LOCK TABLES confs WRITE, topics WRITE, posts WRITE, postdata WRITE, "
@ -706,8 +718,7 @@ class TopicUserContextImpl implements TopicContext
logger.debug("New post number: " + new_post_num);
// Add the post "header" to the posts table.
StringBuffer sql = new StringBuffer("INSERT INTO posts (parent, topicid, num, linecount, creator_uid, "
+ "posted, pseud) VALUES (");
sql.append("INSERT INTO posts (parent, topicid, num, linecount, creator_uid, posted, pseud) VALUES (");
sql.append(parent).append(", ").append(topicid).append(", ").append(new_post_num).append(", ");
sql.append(text_linecount).append(", ").append(env.getUserID()).append(", '");
posted_date = new java.util.Date();
@ -717,9 +728,9 @@ class TopicUserContextImpl implements TopicContext
stmt.executeUpdate(sql.toString());
// Retrieve the new post ID.
ResultSet rs = stmt.executeQuery("SELECT LAST_INSERT_ID();");
rs = stmt.executeQuery("SELECT LAST_INSERT_ID();");
if (!(rs.next()))
throw new InternalStateError("postMessage(): Unable to get new post ID!");
throw new InternalStateError("postNewMessage(): Unable to get new post ID!");
new_post_id = rs.getLong(1);
if (logger.isDebugEnabled())
logger.debug("New post ID: " + new_post_id);
@ -761,6 +772,21 @@ class TopicUserContextImpl implements TopicContext
env.getConference().touchUpdate(conn,posted_date);
env.getConference().touchPost(conn,posted_date);
// Who's subscribed to this conference? Whoever it is, they need to get this post via E-mail.
sql.setLength(0);
sql.append("SELECT uid FROM topicsettings WHERE topicid = ").append(topicid);
sql.append(" AND subscribe = 1;");
if (logger.isDebugEnabled())
logger.debug("SQL: " + sql.toString());
rs = stmt.executeQuery(sql.toString());
while (rs.next())
{ // load the UIDs here
if (mailto_uids==null)
mailto_uids = new ArrayList();
mailto_uids.add(new Integer(rs.getInt(1)));
} // end while
// Fill in our own local variables to reflect the update. This includes the recalculation
// of "unread" based on the new value of "top_message".
int tmp_last_msg = top_message - unread;
@ -782,6 +808,23 @@ class TopicUserContextImpl implements TopicContext
ar = env.newAudit(AuditRecord.POST_MESSAGE,"conf=" + env.getConfID() + ",topic=" + topicid + ",post="
+ new_post_id,"pseud=" + real_pseud);
if (mailto_uids!=null)
{ // We need to translate the "mailto" UIDs to E-mail addresses while we still have the database open!
mailto_addrs = new ArrayList(mailto_uids.size());
sql.setLength(0);
sql.append("SELECT c.email FROM users u, contacts c WHERE u.contactid = c.contactid AND u.uid ");
if (mailto_uids.size()==1)
sql.append("= ").append(mailto_uids.get(0)).append(';');
else
sql.append("IN (").append(StringUtil.join(mailto_uids,", ")).append(");");
if (logger.isDebugEnabled())
logger.debug("SQL: " + sql.toString());
rs = stmt.executeQuery(sql.toString());
while (rs.next())
mailto_addrs.add(rs.getString(1));
} // end if
} // end try
catch (SQLException e)
{ // turn SQLException into data exception
@ -796,6 +839,15 @@ class TopicUserContextImpl implements TopicContext
} // end finally
if (mailto_addrs!=null)
{ // a copy of the post needs to be delivered via E-mail to the specified addresses
// use our PostDeliveryAgent to do it in the background
PostDeliveryAgent agent = new PostDeliveryAgent(env,text,pseud,name,topicnum,mailto_addrs);
agent.start();
} // end if
// else don't bother - it would be a waste of time
// return the new message context
return new TopicMessageUserContextImpl(env,new_post_id,parent,new_post_num,text_linecount,env.getUserID(),
posted_date,real_pseud);
@ -1159,6 +1211,78 @@ class TopicUserContextImpl implements TopicContext
} // end getBozos
public boolean isSubscribed()
{
return subscribed;
} // end isSubscribed
public void setSubscribed(boolean flag) throws DataException
{
if ((subscribed==flag) || deleted || env.getUser().userIsAnonymous())
return; // no-op
Connection conn = null; // pooled database connection
try
{ // get a database connection
conn = env.getConnection();
Statement stmt = conn.createStatement();
stmt.executeUpdate("LOCK TABLES topicsettings WRITE, topics READ;");
try
{ // start by trying to see if we can update topicsettings directly
StringBuffer sql = new StringBuffer("UPDATE topicsettings SET subscribe = ");
sql.append(flag ? '1' : '0').append(" WHERE topicid = ").append(topicid).append(" AND uid = ");
sql.append(env.getUserID()).append(';');
if (stmt.executeUpdate(sql.toString())>0)
{ // that was all we needed - just save the flag and exit
subscribed = flag;
return;
} // end if
// OK, check: Is the topic still there?!?
sql.setLength(0);
sql.append("SELECT topicid from topics WHERE topicid = ").append(topicid).append(';');
ResultSet rs = stmt.executeQuery(sql.toString());
if (!(rs.next()))
{ // the topic's been deleted - bail out
makeDeleted();
return;
} // end if
// OK, just insert a new row into topicsettings, why dontcha...
sql.setLength(0);
sql.append("INSERT INTO topicsettings (topicid, uid, subscribe) VALUES (").append(topicid);
sql.append(", ").append(env.getUserID()).append(", ").append(flag ? '1' : '0').append(");");
stmt.executeUpdate(sql.toString());
subscribed = flag; // successful completion
} // end try
finally
{ // unlock the tables before we go
Statement ulk_stmt = conn.createStatement();
ulk_stmt.executeUpdate("UNLOCK TABLES;");
} // end finally
} // end try
catch (SQLException e)
{ // turn SQLException into data exception
logger.error("DB error setting topic data: " + e.getMessage(),e);
throw new DataException("unable to set topic subscribed status: " + e.getMessage(),e);
} // end catch
finally
{ // make sure we release the connection before we go
env.releaseConnection(conn);
} // end finally
} // end setSubscribed
/*--------------------------------------------------------------------------------
* External operations usable only from within the package
*--------------------------------------------------------------------------------
@ -1278,9 +1402,10 @@ class TopicUserContextImpl implements TopicContext
StringBuffer sql =
new StringBuffer("SELECT t.topicid, t.num, t.creator_uid, t.top_message, t.frozen, t.archived, "
+ "t.createdate, t.lastupdate, t.name, IFNULL(s.hidden,0) AS hidden, "
+ "(t.top_message - IFNULL(s.last_message,-1)) AS unread");
+ "(t.top_message - IFNULL(s.last_message,-1)) AS unread, "
+ "IFNULL(s.subscribe,0) AS subscribe");
if (get_option==ConferenceContext.DISPLAY_ACTIVE)
sql.append(", SIGN(t.top_message - IFNULL(s.last_message,-1)) AS newflag");
sql.append(", GREATEST(SIGN(t.top_message - IFNULL(s.last_message,-1)),0) AS newflag");
sql.append(" FROM topics t LEFT JOIN topicsettings s ON t.topicid = s.topicid AND s.uid = ");
sql.append(env.getUserID()).append(" WHERE t.confid = ").append(env.getConfID());
if (where_clause!=null)
@ -1301,7 +1426,7 @@ class TopicUserContextImpl implements TopicContext
new TopicUserContextImpl(env,rs.getInt(1),rs.getShort(2),rs.getInt(3),rs.getInt(4),
rs.getBoolean(5),rs.getBoolean(6),SQLUtil.getFullDateTime(rs,7),
SQLUtil.getFullDateTime(rs,8),rs.getString(9),rs.getBoolean(10),
rs.getInt(11));
rs.getInt(11),rs.getBoolean(12));
rc.add(top);
} // end while
@ -1341,7 +1466,7 @@ class TopicUserContextImpl implements TopicContext
return new TopicUserContextImpl(env,topicid,rs.getShort(2),rs.getInt(3),rs.getInt(4),rs.getBoolean(5),
rs.getBoolean(6),SQLUtil.getFullDateTime(rs,7),
SQLUtil.getFullDateTime(rs,8),rs.getString(9),rs.getBoolean(10),
rs.getInt(11));
rs.getInt(11),rs.getBoolean(12));
// else fall out and return null
} // end try
@ -1378,7 +1503,8 @@ class TopicUserContextImpl implements TopicContext
StringBuffer sql =
new StringBuffer("SELECT t.topicid, t.num, t.creator_uid, t.top_message, t.frozen, t.archived, "
+ "t.createdate, t.lastupdate, t.name, IFNULL(s.hidden,0) AS hidden, "
+ "(t.top_message - IFNULL(s.last_message,-1)) AS unread FROM topics t "
+ "(t.top_message - IFNULL(s.last_message,-1)) AS unread, "
+ "IFNULL(s.subscribe,0) AS subscribe FROM topics t "
+ "LEFT JOIN topicsettings s ON t.topicid = s.topicid AND s.uid = ");
sql.append(env.getUserID()).append(" WHERE t.confid = ").append(env.getConfID());
sql.append(" AND t.num = ").append(topicnum).append(';');
@ -1391,7 +1517,7 @@ class TopicUserContextImpl implements TopicContext
return new TopicUserContextImpl(env,rs.getInt(1),topicnum,rs.getInt(3),rs.getInt(4),rs.getBoolean(5),
rs.getBoolean(6),SQLUtil.getFullDateTime(rs,7),
SQLUtil.getFullDateTime(rs,8),rs.getString(9),rs.getBoolean(10),
rs.getInt(11));
rs.getInt(11),rs.getBoolean(12));
// else fall out and return null
} // end try
@ -1412,3 +1538,5 @@ class TopicUserContextImpl implements TopicContext
} // end getTopicByNumber
} // end class TopicUserContextImpl

View File

@ -721,7 +721,7 @@ public class VeniceEngineImpl implements VeniceEngine, EngineBackend
LazyTreeLexicon lex = new LazyTreeLexicon((String[])(dictionary_tmp.toArray(new String[0])));
spell_rewriter.addDictionary(lex);
html_configs = new HTMLCheckerConfig[4]; // create the array
html_configs = new HTMLCheckerConfig[5]; // create the array
// Create the HTML checker config used to post body text to the database.
HTMLCheckerConfig cfg = HTMLCheckerCreator.create();
@ -781,6 +781,15 @@ public class VeniceEngineImpl implements VeniceEngine, EngineBackend
cfg.addOutputFilter(html_filter);
html_configs[HTMLC_ESCAPE_BODY_PSEUD] = cfg;
// Create the HTML Checker used to strip HTML from posts that are sent via E-mail.
cfg = HTMLCheckerCreator.create();
cfg.setWordWrapLength((short)55);
cfg.setProcessAngles(true);
cfg.setProcessParens(false);
cfg.setDiscardHTMLTags(true);
cfg.configureNormalTagSet();
html_configs[HTMLC_MAIL_POST] = cfg;
if (logger.isDebugEnabled())
logger.debug("initialize() complete :-)");

View File

@ -44,4 +44,6 @@ public interface CommunityBackend
public abstract boolean env_testPermission(String symbol);
public abstract String env_getCommunityName();
} // end interface CommunityBackend

View File

@ -49,4 +49,6 @@ public interface ConferenceBackend
public abstract int env_getConfLevel();
public abstract String env_getConfName();
} // end interface ConferenceBackend

View File

@ -36,6 +36,7 @@ public interface EngineBackend
public static final int HTMLC_POST_PSEUD = 1;
public static final int HTMLC_PREVIEW_BODY = 2;
public static final int HTMLC_ESCAPE_BODY_PSEUD = 3;
public static final int HTMLC_MAIL_POST = 4;
// Integer parameter indexes
public static final int IP_POSTSPERPAGE = 0;

View File

@ -182,4 +182,16 @@ public class EnvCommunity extends EnvUser
} // end getCommunityDefaultRole
public final String getCommunityName()
{
return comm.env_getCommunityName();
} // end getCommunityName
public final String getCommunityAlias()
{
return comm.realCommunityAlias();
} // end getCommunityAlias
} // end class EnvCommunity

View File

@ -116,5 +116,17 @@ public class EnvConference extends EnvCommunity
} // end getConfID
public final String getConferenceAlias()
{
return conf.realConfAlias();
} // end getConfAlias
public final String getConferenceName()
{
return conf.env_getConfName();
} // end getConfName
} // end class EnvConference

View File

@ -175,4 +175,10 @@ public class EnvEngine
} // end saveAuditRecord
public final String getStockMessage(String key)
{
return engine.getStockMessage(key);
} // end getStockMessage
} // end class EnvEngine

View File

@ -199,6 +199,12 @@ public class EnvUser extends EnvEngine
} // end getUserID
public final String getUserName()
{
return user.realUserName();
} // end getUserName
public final int getUserBaseLevel()
{
return user.realBaseLevel();

View File

@ -216,6 +216,24 @@ public class TopicOperations extends VeniceServlet
} // end if (remove bozo)
if (cmd.equals("SY") || cmd.equals("SN"))
{ // "SY", "SN" - Set subscription status
try
{ // call down to set the topic!
topic.setSubscribed(cmd.equals("SY"));
setMyLocation(request,"topicops?" + locator);
return new ManageTopic(user,comm,conf,topic);
} // end try
catch (DataException de)
{ // there was a database error
return new ErrorBox("Database Error","Database error setting subscription status: " + de.getMessage(),
location);
} // end catch
} // end if (subscription control)
// unrecognized command - load the "Manage Topic menu"
try
{ // return that "Manage Topic" page

View File

@ -173,4 +173,10 @@ public class ManageTopic implements JSPRender
} // end getBozosIterator
public boolean isSubscribed()
{
return topic.isSubscribed();
} // end isSubscribed
} // end class ManageTopic

View File

@ -34,7 +34,19 @@
</FONT><P>
<%= rdat.getStdFontTag(ColorSelectors.CONTENT_FOREGROUND,2) %>
<DIV ALIGN="LEFT"><B>Filtered users:</B></DIV>
<DIV ALIGN="LEFT"><B>Topic Subscription:</B></DIV>
<% if (data.isSubscribed()) { %>
You are currently subscribed to this topic, and will receive all new posts to it via E-mail.<P>
<B><A HREF="<%= rdat.getEncodedServletPath("topicops?" + data.getLocator() + "&cmd=SN") %>">Click Here
to Stop Subscribing To This Topic</A></B>
<% } else { %>
You are not currently subscribed to this topic. When you subscribe to a topic, you will receive all new
posts to that topic via E-mail.<P>
<B><A HREF="<%= rdat.getEncodedServletPath("topicops?" + data.getLocator() + "&cmd=SY") %>">Click Here
to Start Subscribing To This Topic</A></B>
<% } // end if %>
<BR><HR WIDTH="80%">
<DIV ALIGN="LEFT"><B>Filtered Users:</B></DIV>
<% if (data.getNumBozos()>0) { %>
<TABLE BORDER=0 ALIGN=CENTER CELLPADDING=0 CELLSPACING=2>
<% Iterator it = data.getBozosIterator(); %>