浏览代码

chat-fixes

version-14
Achilles Rasquinha 7 年前
父节点
当前提交
2d5d367298
共有 9 个文件被更改,包括 303 次插入332 次删除
  1. +6
    -5
      frappe/chat/doctype/chat_profile/chat_profile.js
  2. +37
    -6
      frappe/chat/doctype/chat_profile/chat_profile.json
  3. +58
    -137
      frappe/chat/doctype/chat_profile/chat_profile.py
  4. +53
    -29
      frappe/chat/doctype/chat_room/chat_room.py
  5. +6
    -4
      frappe/chat/doctype/chat_room_user/chat_room_user.json
  6. +1
    -62
      frappe/core/doctype/user/user.json
  7. +0
    -3
      frappe/core/doctype/user/user.py
  8. +114
    -86
      frappe/public/js/frappe/chat.js
  9. +28
    -0
      socketio.js

+ 6
- 5
frappe/chat/doctype/chat_profile/chat_profile.js 查看文件

@@ -1,8 +1,9 @@
// Copyright (c) 2017, Frappe Technologies and contributors
// For license information, please see license.txt

/* eslint semi: "never" */
frappe.ui.form.on('Chat Profile', {
refresh: function(frm) {

refresh: (form) => {
if ( form.doc.user !== frappe.session.user ) {
form.disable_save(true)
form.set_read_only(true)
}
}
});

+ 37
- 6
frappe/chat/doctype/chat_profile/chat_profile.json 查看文件

@@ -3,7 +3,7 @@
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "CP.#####",
"autoname": "field:user",
"beta": 1,
"creation": "2017-11-13 18:26:57.943027",
"custom": 0,
@@ -13,6 +13,37 @@
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "user",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "User",
"length": 0,
"no_copy": 0,
"options": "User",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@@ -40,7 +71,7 @@
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
@@ -234,13 +265,13 @@
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-12-19 11:23:04.791395",
"modified_by": "achilles@erpnext.com",
"modified": "2018-01-01 18:44:47.226982",
"modified_by": "Administrator",
"module": "Chat",
"name": "Chat Profile",
"name_case": "",
@@ -267,7 +298,7 @@
"write": 1
}
],
"quick_entry": 1,
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,


+ 58
- 137
frappe/chat/doctype/chat_profile/chat_profile.py 查看文件

@@ -1,13 +1,12 @@
# imports - module imports
from frappe.model.document import Document
from frappe import _, _dict # <- the best thing ever happened to frappe
from frappe import _
import frappe

# imports - frappe module imports
from frappe.core.doctype.version.version import get_diff
from frappe.chat.doctype.chat_room.chat_room import get_user_chat_rooms
from frappe.chat.doctype.chat_room import chat_room
from frappe.chat.util import (
get_user_doc,
safe_json_loads,
filter_dict,
dictify
@@ -15,159 +14,81 @@ from frappe.chat.util import (

session = frappe.session

# TODO
# User
# [ ] Deleting a User should also delete its Chat Profile.
# [ ] Ensuring username is mandatory when User has been created.

# Chat Profile
# [x] Link Chat Profile DocType to User when User has been created.
# [x] Once done, add a validator to check Chat Profile has been
# created only once.
# [x] Users can view other Users Chat Profile, but not update the same.
# Not sure, but circular link would be helpful.

class ChatProfile(Document):
# trigger from DocType
def before_save(self):
if not self.is_new():
self.get_doc_before_save()

def on_update(self):
user = get_user_doc()

if user.chat_profile:
if user.chat_profile != self.name:
frappe.throw(_("Sorry! You don't have permission to update this profile."))
else:
if not self.is_new():
before = self.get_doc_before_save()
after = self

diff = dictify(get_diff(before, after))
if diff:
fields = [change[0] for change in diff.changed]

# NOTE: Version DocType is the best thing ever. Selective Updates to Chat Rooms/Users FTW.

# status update are dispatched to current user and Direct Chat Rooms.
if 'status' in fields:
# TODO: you can add filters within get_user_chat_rooms
rooms = get_user_chat_rooms(user)
rooms = [r for r in rooms if r.type == 'Direct']
resp = dict(
user = user.name,
data = dict(
status = self.status
)
)

for room in rooms:
frappe.publish_realtime('frappe.chat.profile:update', resp, room = room.name, after_commit = True)

if 'display_widget' in fields:
resp = dict(
user = user.name,
data = dict(
display_widget = bool(self.display_widget)
)
)
frappe.publish_realtime('frappe.chat.profile:update', resp, user = user.name, after_commit = True)

def get_user_chat_profile_doc(user = None):
user = get_user_doc(user)
prof = frappe.get_doc('Chat Profile', user.chat_profile)

return prof

def get_user_chat_profile(user = None, fields = None):
'''
Returns the Chat Profile for a given user.
'''
user = get_user_doc(user)
prof = get_user_chat_profile_doc(user)

data = dict(
name = user.name,
email = user.email,
first_name = user.first_name,
last_name = user.last_name,
username = user.username,
avatar = user.user_image,
bio = user.bio,

status = prof.status,
chat_bg = prof.chat_background,

notification_tones = bool(prof.notification_tones),
conversation_tones = bool(prof.conversation_tones), # frappe, y u no jsonify 0,1 bools? :(
display_widget = bool(prof.display_widget)
)
if not self.is_new():
b, a = self.get_doc_before_save(), self
diff = dictify(get_diff(a, b))
if diff:
user = session.user

try:
data = filter_dict(data, fields)
except KeyError as e:
frappe.throw(str(e))
fields = [changed[0] for changed in diff.changed]

return data
if 'status' in fields:
rooms = chat_room.get(user, filters = ['type', '=', 'Direct'])
update = dict(user = user, data = dict(status = self.status))

def get_new_chat_profile_doc(user = None, link = True):
user = get_user_doc(user)
prof = frappe.new_doc('Chat Profile')
prof.save()
for room in rooms:
frappe.publish_realtime('frappe.chat.profile:update', update, room = room.name, after_commit = True)

if link:
user.update(dict(
chat_profile = prof.name
))
user.save()
def authenticate(user):
if user != session.user:
frappe.throw(_("Sorry, you're not authorized."))

@frappe.whitelist()
def get(user, fields = None):
duser = frappe.get_doc('User', user)
dprof = frappe.get_doc('Chat Profile', user)

profile = dict(
# User
name = duser.name,
email = duser.email,
first_name = duser.first_name,
last_name = duser.last_name,
username = duser.username,
avatar = duser.user_image,
bio = duser.bio,

# Chat Profile
status = dprof.status,
chat_background = dprof.chat_background,
notification_tones = bool(dprof.notification_tones),
conversation_tones = bool(dprof.conversation_tones),
display_widget = bool(dprof.display_widget)
)
profile = filter_dict(profile, fields)

return prof
return dictify(profile)

@frappe.whitelist()
def create(user, exists_ok = False, fields = None):
'''
Creates a Chat Profile for the current session user, throws error if exists.
'''
exists, fields = safe_json_loads(exists_ok, fields)
user = get_user_doc(user)

if user.name != session.user:
frappe.throw(_("Sorry! You don't have permission to create a profile for user {name}.".format(
name = user.name
)))

if user.chat_profile:
if not exists:
frappe.throw(_("Sorry! You cannot create more than one Chat Profile."))
prof = get_user_chat_profile(user, fields)
else:
prof = get_new_chat_profile_doc(user)
prof = get_user_chat_profile(user, fields)
authenticate(user)

return dictify(prof)
exists_ok, fields = safe_json_loads(exists_ok, fields)

@frappe.whitelist()
def get(user = None, fields = None):
'''
Returns a user's Chat Profile.
'''
fields = safe_json_loads(fields)
prof = get_user_chat_profile(user, fields)
if frappe.db.exists('Chat Profile', user):
if not exists_ok:
frappe.throw(_('Chat Profile for User {user} exists.'.format(user = user)))
else:
dprof = frappe.new_doc('Chat Profile')
dprof.user = user
dprof.save()

return dictify(prof)
profile = get(user, fields = fields)

return profile

@frappe.whitelist()
def update(user, data):
data = safe_json_loads(data)
user = get_user_doc(user)
authenticate(user)

if user.name != session.user:
frappe.throw(_("Sorry! You don't have permission to update Chat Profile for user {name}.".format(
name = user.name
)))
data = safe_json_loads(data)

prof = get_user_chat_profile_doc(user)
prof.update(data)
prof.save()
dprof = frappe.get_doc('Chat Profile', user)
dprof.update(data)
dprof.save(ignore_permissions = True)

+ 53
- 29
frappe/chat/doctype/chat_room/chat_room.py 查看文件

@@ -2,9 +2,9 @@
import json

# imports - module imports
import frappe
from frappe.model.document import Document
from frappe import _, _dict
from frappe import _
import frappe

# imports - frappe module imports
from frappe.core.doctype.version.version import get_diff
@@ -188,7 +188,7 @@ def get_new_chat_room_doc(kind, owner, users = None, name = None):
docs.append(doc)
room.users = docs
room.save()
room.save(ignore_permissions = True)
return room

@@ -253,44 +253,68 @@ def get_user_chat_rooms(user = None, rooms = None, fields = None):
return rooms

@frappe.whitelist()
def create(kind, owner, users = None, name = None):
users = safe_json_loads(users)
if owner != session.user:
frappe.throw(_("Sorry! You're not authorized to create a Chat Room."))

room = get_new_chat_room(kind = kind, owner = owner, users = users, name = name)
room = squashify(room)

users = [room.owner] + [u for u in room.users]
for u in users:
frappe.publish_realtime('frappe.chat.room:create', room,
user = u, after_commit = True)

return room

# Could we move pagination to a config, but how?
# One possibility is to add to Chat Profile itself.
# Actually yes.
@frappe.whitelist()
def get(user, rooms = None, fields = None):
rooms = safe_json_loads(rooms)
fields = safe_json_loads(fields)
def get_history(room, user = None, pagination = 20):
user = get_user_doc(user)
mess = get_messages(room, pagination = pagination)

user = get_user_doc(user)
mess = squashify(mess)
return dictify(mess)

if user.name != frappe.session.user:
frappe.throw(_("You're not authorized to view this room."))
def authenticate(user):
if user != session.user:
frappe.throw(_("Sorry, you're not authorized."))

@frappe.whitelist()
def get(user, rooms = None, fields = None, filters = None):
authenticate(user)

rooms, fields, filters = safe_json_loads(rooms, fields, filters)

data = get_user_chat_rooms(user = user, rooms = rooms, fields = fields)
return data

# Could we move pagination to a config, but how?
# One possibility is to add to Chat Profile itself.
# Actually yes.
@frappe.whitelist()
def get_history(room, user = None, pagination = 20):
user = get_user_doc(user)
mess = get_messages(room, pagination = pagination)
def create(kind, owner, users = None, name = None):
authenticate(owner)

users = safe_json_loads(users)

room = frappe.new_doc('Chat Room')
room.type = kind
room.owner = owner
dusers = [ ]

if users:
for user in users:
duser = frappe.new_doc('Chat Room User')
duser.name = user
dusers.append(duser)
room.users = dusers
room.save(ignore_permissions = True)

mess = squashify(mess)
return dictify(mess)





room = get_new_chat_room(kind = kind, owner = owner, users = users, name = name)
room = squashify(room)

users = [room.owner] + [u for u in room.users]
for u in users:
frappe.publish_realtime('frappe.chat.room:create', room,
user = u, after_commit = True)

return room

+ 6
- 4
frappe/chat/doctype/chat_room_user/chat_room_user.json 查看文件

@@ -3,6 +3,7 @@
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:user",
"beta": 1,
"creation": "2017-11-08 15:24:21.029314",
"custom": 0,
@@ -38,7 +39,7 @@
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
@@ -49,6 +50,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "is_admin",
"fieldtype": "Check",
"hidden": 0,
@@ -79,12 +81,12 @@
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2017-11-28 11:50:06.165435",
"modified": "2018-01-02 12:28:45.145823",
"modified_by": "achilles@erpnext.com",
"module": "Chat",
"name": "Chat Room User",
@@ -92,7 +94,7 @@
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only": 1,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",


+ 1
- 62
frappe/core/doctype/user/user.json 查看文件

@@ -2048,67 +2048,6 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "chat_section_break",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Chat",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "chat_profile",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Chat Profile",
"length": 0,
"no_copy": 0,
"options": "Chat Profile",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
@@ -2124,7 +2063,7 @@
"istable": 0,
"max_attachments": 5,
"menu_index": 0,
"modified": "2017-11-15 13:01:00.085916",
"modified": "2018-01-01 11:11:40.716211",
"modified_by": "Administrator",
"module": "Core",
"name": "User",


+ 0
- 3
frappe/core/doctype/user/user.py 查看文件

@@ -16,9 +16,6 @@ from frappe.limits import get_limits
from frappe.website.utils import is_signup_enabled
from frappe.utils.background_jobs import enqueue

# imports - frappe.chat
from frappe.chat.doctype.chat_profile.chat_profile import get_user_chat_profile_doc

STANDARD_USERS = ("Guest", "Administrator")

class MaxUsersReachedError(frappe.ValidationError): pass


+ 114
- 86
frappe/public/js/frappe/chat.js 查看文件

@@ -1,4 +1,4 @@
// frappe Chat
// Frappe Chat
// Author - Achilles Rasquinha <achilles@frappe.io>

/* eslint semi: "never" */
@@ -6,66 +6,25 @@

// frappe extensions

// frappe.user extensions
// frappe.model extensions
frappe.provide('frappe.model')
/**
* @description Returns the first name of a User.
* @description Subscribe to a model for realtime updates.
*
* @param {string} user - User
* @example
* frappe.model.subscribe('User')
* // Subscribe to all User records
*
* @returns The first name of the user.
* frappe.model.subscribe('User', 'achilles@frappe.io')
* frappe.model.subscribe('User', ['achilles@frappe.io', 'rushabh@frappe.io'])
* // Subscribe to User of name(s)
*
* @example
* frappe.user.first_name("Rahul Malhotra")
* // returns "Rahul"
* frappe.model.subscribe('User', 'achilles@frappe.io', 'username')
* frappe.model.subscribe('User', ['achilles@frappe.io', 'rushabh@frappe.io'], ['email', 'username'])
* // Subscribe to User of name for field(s)
*/
frappe.provide('frappe.user')
frappe.user.first_name = user => frappe.user.full_name(user).split(" ")[0]

// frappe.ui extensions
frappe.provide('frappe.ui')
frappe.ui.Uploader = class
{
constructor (wrapper, options = { })
{
this.options = frappe.ui.Uploader.OPTIONS
this.set_wrapper(wrapper)
this.set_options(options)
}

set_wrapper (wrapper)
{
this.$wrapper = $(wrapper)

return this
}

set_options (options)
{
this.options = { ...this.options, ...options }

return this
}

render ( )
{
const $template = $(frappe.ui.Uploader.TEMPLATE)
this.$wrapper.html($template)
}
}
frappe.ui.Uploader.Layout = { DIALOG: 'DIALOG' }
frappe.ui.Uploader.OPTIONS =
{
layout: frappe.ui.Uploader.Layout.DIALOG
}
frappe.ui.Uploader.TEMPLATE =
`
<div class="foobar">
FooBar
</div>
`

frappe.provide('frappe.ui.keycode')
frappe.ui.keycode = { RETURN: 13 }
frappe.model.subscribe = (doctype, name, field) =>
frappe.realtime.publish('frappe.model:subscribe', { doctype: doctype, name: name, field: field })

/**
* @description The base class for all Frappe Errors.
@@ -208,6 +167,8 @@ frappe.datetime.compare = (a, b) =>
// frappe's utility namespace.
frappe.provide('frappe._')

frappe._.head = arr => frappe._.is_empty(arr) ? undefined : arr[0]

/**
* @description Python-inspired format extension for string objects.
*
@@ -408,9 +369,71 @@ frappe._.is_mobile = () =>
*/
frappe._.compact = array => array.filter(Boolean)

// frappe extensions

// frappe.user extensions
/**
* @description Returns the first name of a User.
*
* @param {string} user - User
*
* @returns The first name of the user.
*
* @example
* frappe.user.first_name("Rahul Malhotra")
* // returns "Rahul"
*/
frappe.provide('frappe.user')
frappe.user.first_name = user => frappe._.head(frappe.user.full_name(user).split(" "))

// frappe.ui extensions
frappe.provide('frappe.ui')
frappe.ui.Uploader = class
{
constructor (wrapper, options = { })
{
this.options = frappe.ui.Uploader.OPTIONS
this.set_wrapper(wrapper)
this.set_options(options)
}

set_wrapper (wrapper)
{
this.$wrapper = $(wrapper)

return this
}

set_options (options)
{
this.options = { ...this.options, ...options }

return this
}

render ( )
{
const $template = $(frappe.ui.Uploader.TEMPLATE)
this.$wrapper.html($template)
}
}
frappe.ui.Uploader.Layout = { DIALOG: 'DIALOG' }
frappe.ui.Uploader.OPTIONS =
{
layout: frappe.ui.Uploader.Layout.DIALOG
}
frappe.ui.Uploader.TEMPLATE =
`
<div class="foobar">
FooBar
</div>
`

frappe.provide('frappe.ui.keycode')
frappe.ui.keycode = { RETURN: 13 }

// frappe.loggers - A registry for frappe loggers.
frappe.provide('frappe.loggers')

/**
* @description Frappe's Logger Class
*
@@ -497,14 +520,9 @@ frappe.provide('frappe.chat.profile')
* @returns {Promise}
*
* @example
* frappe.chat.profile.create((profile) =>
* {
* // do stuff
* })
* frappe.chat.profile.create("status").then((profile) =>
* {
* console.log(profile) // { status: "Online" }
* })
* frappe.chat.profile.create(console.log)
*
* frappe.chat.profile.create("status").then(console.log) // { status: "Online" }
*/
frappe.chat.profile.create = (fields, fn) =>
{
@@ -530,6 +548,35 @@ frappe.chat.profile.create = (fields, fn) =>
})
}

/**
* @description Create a Chat Profile.
*
* @param {string|array} fields - (Optional) fields to be retrieved after creating a Chat Profile.
* @param {function} fn - (Optional) callback with the returned Chat Profile.
*
* @returns {Promise}
*
* @example
* frappe.chat.profile.create(console.log)
*
* frappe.chat.profile.create("status").then(console.log) // { status: "Online" }
*/
frappe.chat.profile.update = (user, update, fn) =>
{
return new Promise(resolve =>
{
frappe.call("frappe.chat.doctype.chat_profile.chat_profile.update",
{ user: user || frappe.session.user, data: update },
response =>
{
if ( fn )
fn(response.message)
resolve(response.message)
})
})
}

// frappe.chat.profile.on
frappe.provide('frappe.chat.profile.on')

@@ -1373,11 +1420,9 @@ class extends Component
}

make ( ) {
frappe.chat.profile.create([
"status", "display_widget", "notification_tones", "conversation_tones"
], profile =>
frappe.chat.profile.create(["status", "display_widget", "notification_tones", "conversation_tones"]).then(profile =>
{
frappe.log.info(`Chat Profile created for User ${frappe.session.user}.`)
frappe.log.info(`Chat Profile created for User ${frappe.session.user} - ${JSON.stringify(profile)}.`)
this.set_state({ profile })

frappe.chat.room.get(rooms =>
@@ -2538,20 +2583,3 @@ class extends Component {
// frappe.components.Select.Option props
// same as frappe.components.Select.

frappe.chat.profile.update
=
function (user, update, fn)
{
return new Promise(resolve =>
{
frappe.call("frappe.chat.doctype.chat_profile.chat_profile.update",
{ user: user || frappe.session.user, data: update },
response =>
{
if ( fn )
fn(response.message)
resolve(response.message)
})
})
}

+ 28
- 0
socketio.js 查看文件

@@ -62,6 +62,33 @@ io.on('connection', function (socket) {
socket.user = cookie.parse(socket.request.headers.cookie).user_id;
socket.files = {};

// frappe namespace (temporary till webpack comes in, we can then reuse).
const frappe = { };
frappe._ = { };

frappe._.compact = function (arr) { return arr.filter(Boolean) }

// frappe.model
// Realtime Database Updates FTW!
function get_model_room (doctype, name, field) {
const site = get_site_name(socket)
const room = frappe._.compact([site, doctype, name, field]).join(":")

return room
}
socket.on('frappe.model:subscribe', function (params) {
const doctype = params.doctype
const name = params.name
const field = params.field

const room = get_model_room(doctype, name, field)

console.log('frappe.model: Subscribing ' + socket.user + ' to room ' + room);
socket.join(room);
})
// end frappe.model

// frappe.chat
socket.on("frappe.chat.room:subscribe", function (rooms) {
if (!Array.isArray(rooms)) {
@@ -87,6 +114,7 @@ io.on('connection', function (socket) {
user: user
});
});
// end frappe.chat

request.get(get_url(socket, '/api/method/frappe.async.get_user_info'))
.type('form')


正在加载...
取消
保存