You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
435 lines
15 KiB
435 lines
15 KiB
package de.srsoftware.widerhall.data; |
|
|
|
import de.srsoftware.widerhall.Configuration; |
|
import de.srsoftware.widerhall.Util; |
|
import org.slf4j.Logger; |
|
import org.slf4j.LoggerFactory; |
|
import org.stringtemplate.v4.ST; |
|
|
|
import javax.mail.MessagingException; |
|
import java.io.UnsupportedEncodingException; |
|
import java.sql.ResultSet; |
|
import java.sql.SQLException; |
|
import java.util.*; |
|
|
|
import static de.srsoftware.widerhall.Constants.*; |
|
import static de.srsoftware.widerhall.Util.t; |
|
|
|
/** |
|
* @author Stephan Richter |
|
* This class embodies the ListMembers table |
|
*/ |
|
public class ListMember { |
|
public static final String TABLE_NAME = "ListMembers"; |
|
public static final int STATE_OWNER = 1; |
|
public static final int STATE_SUBSCRIBER = 2; |
|
public static final int STATE_AWAITING_CONFIRMATION = 4; |
|
public static final int STATE_MODERATOR = 8; |
|
private static final Logger LOG = LoggerFactory.getLogger(ListMember.class); |
|
private static final String LIST_EMAIL = "list_email"; |
|
private static final String USER_EMAIL = "user_email"; |
|
private static final String STATE = "state"; |
|
|
|
private MailingList list; |
|
private User user; |
|
private final String token; |
|
private final int state; |
|
|
|
/** |
|
* create a new list member object |
|
* @param list |
|
* @param user |
|
* @param state |
|
* @param token |
|
*/ |
|
public ListMember(MailingList list, User user, int state, String token){ |
|
this.list = list; |
|
this.user = user; |
|
this.state = state; |
|
this.token = token; |
|
} |
|
|
|
public String addNewModerator(String userEmail) { |
|
if (!isAllowedToEditMods()) return t("You are not allowed to nominate new mods for {}!",list.email()); |
|
User moderator = null; |
|
try { |
|
moderator = User.load(userEmail); |
|
} catch (SQLException e) { |
|
LOG.warn("Failed to load user for {}!",userEmail,e); |
|
return t("Failed to load user for {}!",userEmail); |
|
} |
|
if (moderator == null) return t("No such user: {}",userEmail); |
|
|
|
ListMember member = null; |
|
try { |
|
member = ListMember.load(list,moderator); |
|
} catch (SQLException e) { |
|
LOG.warn("Failed to load list member for {}/{}!",moderator.email(),list.email(),e); |
|
return t("Failed to load list member for {}/{}!",moderator.email(),list.email()); |
|
} |
|
try { |
|
if (member == null) { |
|
ListMember.create(list, moderator, STATE_MODERATOR); |
|
} else { |
|
member.setState(Util.unset(member.state|STATE_MODERATOR,STATE_AWAITING_CONFIRMATION)); |
|
} |
|
} catch (SQLException e) { |
|
LOG.warn("Failed to make {} a moderator of {}",moderator.email(),list.email(),e); |
|
return t("Failed to make {} a moderator of {}",moderator.email(),list.email()); |
|
} |
|
return null; |
|
} |
|
|
|
/** |
|
* tries to confirm the token: |
|
* This method loads the list member, that is assigned with the token. |
|
* If no db entry is found for the token, null is returned. |
|
* If an entry is found, the respective User object is loaded. |
|
* If no User object is assigned, null is returned. |
|
* If a matching User object is present, the member's state is altered from AWAITING_CONFIRMATION to SUBSCRIBER and the token is dropped. |
|
* @param token |
|
* @return |
|
* @throws SQLException |
|
*/ |
|
public static ListMember confirm(String token) throws SQLException { |
|
var rs = Database.open().select(TABLE_NAME).where(TOKEN,token).compile().exec(); |
|
if (rs.next()){ |
|
var lm = ListMember.from(rs); |
|
rs.close(); |
|
if (lm.user != null){ |
|
int newState = lm.state ^ STATE_AWAITING_CONFIRMATION | STATE_SUBSCRIBER; |
|
Database.open() |
|
.update(TABLE_NAME) |
|
.set(TOKEN,null) |
|
.set(STATE, newState) //drop confirmation state, set subscriber state |
|
.where(LIST_EMAIL,lm.list.email()) |
|
.where(USER_EMAIL,lm.user.email()) |
|
.compile() |
|
.run(); |
|
} |
|
return lm; |
|
} |
|
return null; |
|
} |
|
|
|
/** |
|
* Create a new list member entry in the database. |
|
* If the member has the state AWAITING_CONFIRMATION, a token is assigned with the member, too. |
|
* @param list |
|
* @param user |
|
* @param state |
|
* @return |
|
* @throws SQLException |
|
*/ |
|
public static ListMember create(MailingList list, User user, int state) throws SQLException { |
|
String token = null; |
|
if ((state & STATE_AWAITING_CONFIRMATION) > 0){ |
|
token = Util.sha256(String.join("/",list.email(),user.email(),user.salt())); |
|
} |
|
return new ListMember(list,user,state,token).save(); |
|
} |
|
|
|
/** |
|
* create the table for ListMember objects |
|
* @throws SQLException |
|
*/ |
|
public static void createTable() throws SQLException { |
|
var sql = new StringBuilder() |
|
.append("CREATE TABLE ").append(TABLE_NAME) |
|
.append(" (") |
|
.append(LIST_EMAIL).append(" ").append(VARCHAR).append(", ") |
|
.append(USER_EMAIL).append(" ").append(VARCHAR).append(", ") |
|
.append(STATE).append(" ").append(INT).append(", ") |
|
.append(TOKEN).append(" ").append(VARCHAR).append(", ") |
|
.append("PRIMARY KEY (").append(LIST_EMAIL).append(", ").append(USER_EMAIL).append("));"); |
|
Database.open().query(sql).compile().run(); |
|
} |
|
|
|
public String dropMember(String userEmail) { |
|
if (!isModerator()) return t("You are not allowed to remove members of {}!",list.email()); |
|
User user = null; |
|
try { |
|
user = User.load(userEmail); |
|
} catch (SQLException e) { |
|
LOG.warn("Failed to load user for {}!",userEmail,e); |
|
return t("Failed to load user for {}!",userEmail); |
|
} |
|
if (user == null) return t("No such user: {}",userEmail); |
|
|
|
ListMember member = null; |
|
try { |
|
member = ListMember.load(list,user); |
|
} catch (SQLException e) { |
|
LOG.warn("Failed to load list member for {}/{}!",user.email(),list.email(),e); |
|
return t("Failed to load list member for {}/{}!",user.email(),list.email()); |
|
} |
|
|
|
if (member == null) return t("{} is no member of {}!",user.email(),list.email()); |
|
if (member.isOwner()) return t("You are not allowed to remove the list owner!"); |
|
|
|
try { |
|
member.unsubscribe(); |
|
} catch (SQLException e) { |
|
LOG.warn("Failed to un-subscribe {} from {}!",user.email(),list.email(),e); |
|
return t("Failed to un-subscribe {} from {}!",user.email(),list.email()); |
|
} |
|
return null; |
|
} |
|
|
|
public String dropModerator(String userEmail) { |
|
if (!isAllowedToEditMods()) return t("You are not allowed to edit mods of {}!",list.email()); |
|
User moderator = null; |
|
try { |
|
moderator = User.load(userEmail); |
|
} catch (SQLException e) { |
|
LOG.warn("Failed to load user for {}!",userEmail,e); |
|
return t("Failed to load user for {}!",userEmail); |
|
} |
|
if (moderator == null) return t("No such user: {}",userEmail); |
|
|
|
ListMember member = null; |
|
try { |
|
member = ListMember.load(list,moderator); |
|
} catch (SQLException e) { |
|
LOG.warn("Failed to load list member for {}/{}!",moderator.email(),list.email(),e); |
|
return t("Failed to load list member for {}/{}!",moderator.email(),list.email()); |
|
} |
|
try { |
|
if (member == null) { |
|
ListMember.create(list, moderator, ListMember.STATE_SUBSCRIBER); |
|
} else { |
|
member.setState(Util.unset(member.state,STATE_MODERATOR)); |
|
} |
|
} catch (SQLException e) { |
|
LOG.warn("Failed to make {} a subscriber of {}!",moderator.email(),list.email(),e); |
|
return t("Failed to make {} a subscriber of {}!",moderator.email(),list.email()); |
|
} |
|
return null; |
|
} |
|
|
|
/** |
|
* create a new ListMember object from a ResultSet |
|
* @param rs |
|
* @return |
|
* @throws SQLException |
|
*/ |
|
public static ListMember from(ResultSet rs) throws SQLException { |
|
return new ListMember( |
|
MailingList.load(rs.getString(LIST_EMAIL)), |
|
User.load(rs.getString(USER_EMAIL)), |
|
rs.getInt(STATE), |
|
rs.getString(TOKEN)); |
|
} |
|
|
|
/** |
|
* test, if the current object has a given state |
|
* @param testState |
|
* @return |
|
*/ |
|
public boolean hasState(int testState) { |
|
return (state & testState) > 0; |
|
} |
|
|
|
public boolean isAllowedToEditMods(){ |
|
if (isOwner()) return true; |
|
if (isModerator()) return list.modsMayEditMods(); |
|
return false; |
|
} |
|
|
|
public boolean isAwaiting(){ |
|
return hasState(STATE_AWAITING_CONFIRMATION); |
|
} |
|
|
|
public boolean isModerator() { |
|
return hasState(STATE_OWNER|STATE_MODERATOR); |
|
} |
|
|
|
public boolean isOwner(){ |
|
return hasState(STATE_OWNER); |
|
} |
|
|
|
public boolean isSubscriber(){ |
|
return hasState(STATE_SUBSCRIBER|STATE_MODERATOR|STATE_OWNER); |
|
} |
|
|
|
public MailingList list(){ |
|
return list; |
|
} |
|
/** |
|
* return a set of list emails of MailingLists the given user attends |
|
* @param user |
|
* @return |
|
*/ |
|
public static Set<ListMember> listsOf(User user) { |
|
var listEmails = new HashSet<String>(); |
|
try { |
|
var request = Database.open() |
|
.select(TABLE_NAME); |
|
if (!user.hashPermission(User.PERMISSION_ADMIN)) request = request.where(USER_EMAIL, user.email()); |
|
var rs = request.compile().exec(); |
|
while (rs.next()) listEmails.add(rs.getString(LIST_EMAIL)); |
|
} catch (SQLException e) { |
|
LOG.warn("Collecting lists of {} failed: ",user.email(),e); |
|
} |
|
var lists = MailingList.loadAll(listEmails); |
|
var result = new HashSet<ListMember>(); |
|
try { |
|
for (var ml : lists) result.add(ListMember.load(ml,user)); |
|
} catch (SQLException e) { |
|
e.printStackTrace(); |
|
} |
|
|
|
return result; |
|
} |
|
|
|
/** |
|
* return a set of list emails of MailingLists owned by the given user |
|
* @param user |
|
* @return |
|
*/ |
|
public static Set<String> listsOwnedBy(User user) { |
|
var list = new HashSet<String>(); |
|
try { |
|
var request = Database.open().select(TABLE_NAME, LIST_EMAIL, STATE+" & "+STATE_OWNER+" as "+STATE); |
|
if (!user.hashPermission(User.PERMISSION_ADMIN)) request = request.where(USER_EMAIL, user.email()).where(STATE, STATE_OWNER); |
|
var rs = request.compile().exec(); |
|
while (rs.next()) list.add(rs.getString(LIST_EMAIL)); |
|
} catch (SQLException e) { |
|
LOG.warn("Collecting lists of {} failed: ",user.email(),e); |
|
} |
|
return list; |
|
} |
|
|
|
/** |
|
* load the list member specified by a MailingList and as User object |
|
* @param list |
|
* @param user |
|
* @return |
|
* @throws SQLException |
|
*/ |
|
public static ListMember load(MailingList list,User user) throws SQLException { |
|
if (list == null || user == null) return null; |
|
var rs = Database |
|
.open() |
|
.select(TABLE_NAME) |
|
.where(LIST_EMAIL,list.email()) |
|
.where(USER_EMAIL,user.email()) |
|
.compile() |
|
.exec(); |
|
try { |
|
if (rs.next()) return ListMember.from(rs); |
|
} finally { |
|
rs.close(); |
|
} |
|
return null; |
|
} |
|
|
|
|
|
public static Set<ListMember> of(MailingList list) throws SQLException { |
|
var rs = Database.open().select(TABLE_NAME).where(LIST_EMAIL,list.email()).compile().exec(); |
|
var set = new HashSet<ListMember>(); |
|
try { |
|
while (rs.next()) set.add(ListMember.from(rs)); |
|
} finally { |
|
rs.close(); |
|
} |
|
return set; |
|
} |
|
|
|
public Map<String,Object> safeMap(){ |
|
return Map.of( |
|
EMAIL,user.email(), |
|
NAME,user.name(), |
|
STATE,stateText() |
|
); |
|
} |
|
|
|
/** |
|
* Save the current ListMember object to the database |
|
* @return |
|
* @throws SQLException |
|
*/ |
|
private ListMember save() throws SQLException { |
|
var req = Database.open() |
|
.insertInto(TABLE_NAME) |
|
.set(LIST_EMAIL,list.email()) |
|
.set(USER_EMAIL,user.email()) |
|
.set(STATE,state); |
|
if (token != null) req.set(TOKEN,token); |
|
req.compile().run(); |
|
return this; |
|
} |
|
|
|
public void sendConfirmationMail(ST template) throws SQLException, MessagingException { |
|
var subject = t("[{}] Subscription complete!",list.name()); |
|
var data = new HashMap<String,Object>(); |
|
data.put(USER,user.safeMap()); |
|
data.put(LIST,list.minimalMap()); |
|
data.put(URL,Configuration.instance().baseUrl()+"/web/index"); |
|
if (list.isOpenForSubscribers()) data.put("open_list",true); |
|
var text = template.add("data",data).render(); |
|
try { |
|
list.smtp().send(list.email(),list.name(),user.email(),subject,text); |
|
} catch (UnsupportedEncodingException e) { |
|
LOG.warn("Failed to send list subscription confirmation!",e); |
|
} |
|
} |
|
|
|
public ListMember setState(int newState) throws SQLException { |
|
Database.open() |
|
.update(TABLE_NAME) |
|
.set(STATE,newState) |
|
.where(USER_EMAIL,user.email()) |
|
.where(LIST_EMAIL,list.email()) |
|
.compile() |
|
.run(); |
|
return this; |
|
} |
|
|
|
/** |
|
* convert state flag to readable text |
|
* @return |
|
*/ |
|
public String stateText() { |
|
var words = new TreeSet<String>(); |
|
if (isAwaiting()) words.add(t("awaiting confirmation")); |
|
if (isModerator()) words.add(t("moderator")); |
|
if (isOwner()) words.add(t("owner")); |
|
if (isSubscriber()) words.add(t("subscriber")); |
|
return String.join(", ",words); |
|
} |
|
|
|
/** |
|
* get the token of the current list member |
|
* @return |
|
*/ |
|
public String token() { |
|
return token; |
|
} |
|
|
|
/** |
|
* unsubscribe a list member |
|
* @throws SQLException |
|
*/ |
|
public void unsubscribe() throws SQLException { |
|
var db = Database.open(); |
|
var rs = db.select(TABLE_NAME) |
|
.where(LIST_EMAIL,list.email()) |
|
.where(USER_EMAIL,user.email()) |
|
.compile() |
|
.exec(); |
|
while (rs.next()){ |
|
int state = Util.unset(rs.getInt(STATE),STATE_SUBSCRIBER,STATE_MODERATOR,STATE_AWAITING_CONFIRMATION); // drop subscription and awaiting flags |
|
var req = state < 1 ? db.deleteFrom(TABLE_NAME) : db.update(TABLE_NAME).set(STATE,state).set(TOKEN,null); |
|
req.where(LIST_EMAIL,list.email()).where(USER_EMAIL,user.email()).compile().run(); |
|
} |
|
} |
|
|
|
public static void updateListEmail(String oldEmail, String newEmail) throws SQLException { |
|
Database.open().update(TABLE_NAME).set(LIST_EMAIL,newEmail).where(LIST_EMAIL,oldEmail).compile().run(); |
|
} |
|
|
|
public User user(){ |
|
return user; |
|
} |
|
}
|
|
|